package mage.client.util.audio; import java.util.ArrayDeque; import java.util.HashSet; import java.util.LinkedList; import java.util.Queue; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.DataLine; import javax.sound.sampled.LineEvent.Type; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.Mixer; import javax.sound.sampled.SourceDataLine; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import mage.utils.ThreadUtils; public class LinePool { private final Logger log = LoggerFactory.getLogger(getClass()); private static final int LINE_CLEANUP_INTERVAL = 30000; private final Queue<SourceDataLine> freeLines = new ArrayDeque<>(); private final Queue<SourceDataLine> activeLines = new ArrayDeque<>(); private final Set<SourceDataLine> busyLines = new HashSet<>(); private final LinkedList<MageClip> queue = new LinkedList<>(); /* * Initially all the lines are in the freeLines pool. When a sound plays, one line is being selected randomly from * the activeLines and then, if it's empty, from the freeLines pool and used to play the sound. The line is moved to * busyLines. When a sound stops, the line is moved to activeLines if it contains <= elements than alwaysActive * parameter, else it's moved to the freeLines pool. Every 30 seconds the lines in the freeLines pool are closed * from the timer thread to prevent deadlocks in PulseAudio internals. */ private final Mixer mixer; private final int alwaysActive; public LinePool() { this(new AudioFormat(22050, 16, 1, true, false), 4, 1); } public LinePool(AudioFormat audioFormat, int size, int alwaysActive) { this.alwaysActive = alwaysActive; mixer = AudioSystem.getMixer(null); DataLine.Info lineInfo = new DataLine.Info(SourceDataLine.class, audioFormat); for (int i = 0; i < size; i++) { try { SourceDataLine line = (SourceDataLine) mixer.getLine(lineInfo); freeLines.add(line); } catch (LineUnavailableException e) { log.warn("Failed to get line from mixer", e); } } new Timer("Line cleanup", true).scheduleAtFixedRate(new TimerTask() { @Override public void run() { synchronized (LinePool.this) { for (SourceDataLine sourceDataLine : freeLines) { if (sourceDataLine.isOpen()) { sourceDataLine.close(); log.debug("Closed line {}", sourceDataLine); } } } } }, LINE_CLEANUP_INTERVAL, LINE_CLEANUP_INTERVAL); } private synchronized SourceDataLine borrowLine() { SourceDataLine line = activeLines.poll(); if (line == null) { line = freeLines.poll(); } if (line != null) { busyLines.add(line); } return line; } private synchronized void returnLine(SourceDataLine line) { busyLines.remove(line); if (activeLines.size() < alwaysActive) { activeLines.add(line); } else { freeLines.add(line); } } public void playSound(final MageClip mageClip) { final SourceDataLine line; synchronized (LinePool.this) { log.debug("Playing {}", mageClip.getFilename()); logLineStats(); line = borrowLine(); if (line == null) { // no lines available, queue sound to play it when a line is available queue.add(mageClip); log.debug("Sound {} queued.", mageClip.getFilename()); return; } logLineStats(); } ThreadUtils.threadPool.submit(() -> { synchronized (LinePool.this) { try { if (!line.isOpen()) { line.open(); line.addLineListener(event -> { log.debug("Event: {}", event); if (event.getType() != Type.STOP) { return; } synchronized (LinePool.this) { log.debug("Before stop on line {}", line); logLineStats(); returnLine(line); log.debug("After stop on line {}", line); logLineStats(); MageClip queuedSound = queue.poll(); if (queuedSound != null) { log.debug("Playing queued sound {}", queuedSound); playSound(queuedSound); } } }); } line.start(); } catch (LineUnavailableException e) { log.warn("Failed to open line", e); } } byte[] buffer = mageClip.getBuffer(); log.debug("Before write to line {}", line); line.write(buffer, 0, buffer.length); line.drain(); line.stop(); log.debug("Line completed: {}", line); }); } private void logLineStats() { log.debug("Free lines: {} Active: {} Busy: {}", freeLines.size(), activeLines.size(), busyLines.size()); } }